package com.bluexmicro.ota.asset

import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.content.Context
import com.bluexmicro.bluetooth.BluetoothCentral
import com.bluexmicro.bluetooth.model.ConnectionPriority
import com.bluexmicro.bluetooth.model.PhyType
import com.bluexmicro.bluetooth.model.ScanMode
import com.bluexmicro.bluetooth.model.WriteType
import com.bluexmicro.bluetooth.parZip
import com.bluexmicro.bluetooth.peripheral.ConnectionState
import com.bluexmicro.bluetooth.peripheral.PeripheralManager
import com.bluexmicro.bluetooth.toUInt8
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.UUID
import kotlin.jvm.Throws
import kotlin.math.max
import kotlin.math.min

class AssetProcess {

    private var chunkedSize = 20

    companion object {
        private val UUID_BX_OTA_SERVICE = UUID.fromString("0000ee00-d102-e111-9b23-00025b00a5c7")
        private val UUID_BX_COMMAND = UUID.fromString("0000ee01-d102-e111-9b23-00025b00a5c7")
        private val UUID_BX_DATA = UUID.fromString("0000ee02-d102-e111-9b23-00025b00a5c7")

        private const val CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"
        private val UUID_CLIENT_CHARACTERISTIC_CONFIG =
            UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG)

        private const val COMMAND_PROPERTIES =
            BluetoothGattCharacteristic.PROPERTY_NOTIFY or BluetoothGattCharacteristic.PROPERTY_WRITE
        private const val DATA_PROPERTIES =
            BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE

        const val CMD_START = 7
        const val CMD_END = 8
        private const val CMD_CHECK_RECOVERY = 2

        private const val CMD_EXIT = 4
    }


    @Throws(Exception::class)
    suspend fun initialize(context: Context, device: BluetoothDevice): Process {
        val triple = setupPeripheral(context, device)
        //可能要更换对象
        val peripheral = triple.first
        val commandHandler = triple.second
        val dataHandler = triple.third

        val recoveryMode = fetchRecoveryMode(peripheral, commandHandler)

        if (recoveryMode) {
            throw IllegalStateException("Cannot initialize in Recovery Mode")
        }
        return Process(peripheral, commandHandler, dataHandler)
    }

    inner class Process constructor(
        private val peripheral: PeripheralManager.BluetoothPeripheral,
        private val commandHandler: BluetoothGattCharacteristic,
        private val dataHandler: BluetoothGattCharacteristic
    ) {

        fun update(asset: AssetBean) = flow {
            if (requestStart(peripheral, commandHandler, asset).not()) {
                throw IllegalStateException("Remote Device reject start")
            }

            val checkout = asset.rawData.size.run {
                val tmp = (this.toFloat() / 1024 / 1024 * 500).toLong()
                max(5000L, tmp)
            }

            val pending = asset.rawData.toList().chunked(chunkedSize)
            var progress = 0
            for (i in pending.indices) {
                val byteArray = pending[i].toByteArray()
                peripheral.write(dataHandler, WriteType.WithoutResponse, byteArray)
                val tmp = i.toFloat() / pending.size
                val tmp2 = (tmp * 100).toInt()
                if (tmp2 != progress) {
                    progress = tmp2
                    emit(tmp)
                }
            }
            if (requestEnd(peripheral, commandHandler, checkout).not()) {
                throw IllegalStateException("Remote Device reject end")
            }
            emit(1f)
        }

        /**
         * 当主工程蓝牙断开时
         */
        fun release() {
            BluetoothCentral.releasePeripheral(peripheral)
        }
    }

    private suspend fun setupPeripheral(
        context: Context,
        device: BluetoothDevice
    ): Triple<PeripheralManager.BluetoothPeripheral, BluetoothGattCharacteristic, BluetoothGattCharacteristic> {
        BluetoothCentral.setup(context)

        //初始化设备
        val peripheral = BluetoothCentral.retrievePeripherals(context, device.address)
            ?: throw IllegalArgumentException("mac address invalid or please scan device first")

        //连接设备
        var connected = peripheral.state.value == ConnectionState.CONNECTED
        if (connected.not()) {
            for (i in 0..1) {
                //todo timeout
                BluetoothCentral.connect(context, peripheral)
                if (peripheral.state.value == ConnectionState.CONNECTED) {
                    connected = true
                    break
                }
            }
        }
        if (!connected) throw IllegalStateException("Cannot connect device with ${device.address}")

        try {
            val size = peripheral.requestMTU(251).getOrNull() ?: 23
            //在Android S以上版本，越是接近最高速度失败率越高
            chunkedSize = max(20, size - 23)

            //设置连接参数，不成功也是可以的，但是会影响速度
            peripheral.requestConnectionPriority(ConnectionPriority.HIGH)
            //同上
            peripheral.requestPhy(PhyType.LE_2M.mask, PhyType.LE_2M.mask)
        } catch (e: Exception) {
            e.printStackTrace()
        }

        //初始化服务特征
        val services = peripheral.discoverServices().getOrNull()
        val service = services?.find { it.uuid.equals(UUID_BX_OTA_SERVICE) }
        val command = service?.getCharacteristic(UUID_BX_COMMAND)
        val data = service?.getCharacteristic(UUID_BX_DATA)
        if (command == null || data == null) throw IllegalStateException("Invalid Device")
        val flag1 = command.properties and COMMAND_PROPERTIES > 0
        val flag2 = data.properties and DATA_PROPERTIES > 0
        if (flag1.not() || flag2.not()) throw IllegalStateException("Invalid Device")

        //开启Notification
        peripheral.setNotification(command, true)
        val descriptor = command.getDescriptor(UUID_CLIENT_CHARACTERISTIC_CONFIG)
            ?: throw IllegalStateException("Invalid Device")
        peripheral.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
        return Triple(peripheral, command, data)
    }


    private suspend fun fetchRecoveryMode(
        peripheral: PeripheralManager.BluetoothPeripheral,
        commandHandler: BluetoothGattCharacteristic
    ): Boolean {
        val cmd = byteArrayOf(CMD_CHECK_RECOVERY.toByte(), 0x00.toByte())
        val rsp = request(peripheral, commandHandler, cmd)
        return rsp[2].toUInt8() == 0
    }

    private suspend fun exitRecoveryMode(
        peripheral: PeripheralManager.BluetoothPeripheral,
        commandHandler: BluetoothGattCharacteristic
    ): Boolean {
        val cmd = byteArrayOf(CMD_EXIT.toByte(), 0x00.toByte(), 0x02)
        val rsp = request(peripheral, commandHandler, cmd)
        return rsp[2].toUInt8() == 0
    }


    private suspend fun requestStart(
        peripheral: PeripheralManager.BluetoothPeripheral,
        commandHandler: BluetoothGattCharacteristic,
        asset: AssetBean
    ): Boolean {
        val bb = ByteBuffer.allocate(11).order(ByteOrder.LITTLE_ENDIAN)
        bb.put(CMD_START.toByte())
            .put(0)
            .put(asset.type.code)
            .putInt(asset.rawData.size)
            .putInt(asset.crc32)
        val rsp = request(peripheral, commandHandler, bb.array())
        return rsp[2].toUInt8() == 0
    }

    private suspend fun requestEnd(
        peripheral: PeripheralManager.BluetoothPeripheral,
        commandHandler: BluetoothGattCharacteristic,
        timeout: Long = 5000L
    ): Boolean {
        val cmd = byteArrayOf(CMD_END.toByte(), 0x00.toByte())
        val rsp = request(peripheral, commandHandler, cmd, timeout)
        return rsp[2].toUInt8() == 0
    }


    private suspend fun request(
        peripheral: PeripheralManager.BluetoothPeripheral,
        commandHandler: BluetoothGattCharacteristic,
        cmd: ByteArray,
        timeout: Long = 5000L
    ): ByteArray {
        return withTimeout(timeout) {
            parZip(
                { peripheral.write(commandHandler, WriteType.WithResponse, cmd) },
                {
                    BluetoothCentral.observeCharacteristicValue(
                        peripheral.device.address,
                        commandHandler.service.uuid.toString(),
                        commandHandler.uuid.toString()
                    ).first {
                        it.value.size >= 3 && it.value[1].toUInt8() == 0x80 && it.value[0] == cmd[0]
                    }
                }
            ) { _, bluetoothValue ->
                bluetoothValue.value
            }
        }
    }
}